package il.ac.technion.wiki.impl;

import il.ac.technion.wiki.api.IContentImporter;
import il.ac.technion.wiki.api.IContentRanker;
import il.ac.technion.wiki.api.IDataStore;
import il.ac.technion.wiki.api.IContentImporter.ContentException;
import il.ac.technion.wiki.api.IDataStore.DataStoreException;
import il.ac.technion.wiki.api.IDataStore.NotFoundException;
import il.ac.technion.wiki.api.AbstractWikiEngine;
import il.ac.technion.wiki.api.search.ISearchExpression;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.EnumMap;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Map;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Defines a concrete Wiki engine.
 */
public class WikiEngine extends AbstractWikiEngine {
	/**
	 * Create a new instance of the wiki engine.
	 * @param dataStore	The underlying data-store to use for storing the wiki's
	 * 					content.
	 * @param importers	An array of objects that will be used for importing
	 * 					content into this wiki. This can be modified later by use
	 * 					of the {@link #addImporter} and {@link #removeImporter} methods.
	 * @param ranker	Specifies a content ranker to be used for ranking content relevance
	 * 					during search.
	 */
	public WikiEngine(IDataStore dataStore, IContentImporter[] importers, IContentRanker ranker) {
		assert dataStore!=null;
		assert importers!=null;
		assert ranker!=null;
		
		this.dataStore = dataStore;
		
		for (IContentImporter importer : importers)
			for (String mimeType: importer.handledContentTypes())
				importerMap.put(mimeType, importer);
		
		this.ranker = ranker;
	}
	
	@Override
	public WikiContent importContent(byte[] content, String contentType, String name) throws ContentException {
		IContentImporter importer = importerMap.get(contentType);
	
		if (importer == null)
			throw new ContentException("no importer for " + contentType);
		
		WikiContent wc = new WikiContent();
		wc.setName(name);
		wc.setText(importer.process(content));
		return wc;
	}
	
	@Override
	public WikiContent get(String name) throws NotFoundException,
			DataStoreException {
		return (WikiContent) dataStore.get(name, 0);
	}
	
	@Override
	protected SortedMap<Integer, WikiContent> search(ISearchExpression expr) throws DataStoreException, BadQueryException {
		SortedMap<Integer, WikiContent> results = new TreeMap<Integer, WikiContent>();
		for (IWikiContent wc: this.dataStore.search(expr, 0)){
			results.put(ranker.rank(wc, expr), (WikiContent) wc);			
		}
		return results;
	}

	@Override
	public void setRanker(IContentRanker ranker) {
		assert ranker!=null;
		this.ranker = ranker;
	}
	
	@Override
	public void removeImporter(String contentType) {
		importerMap.remove(contentType);
	}
	
	@Override
	public void addImporter(IContentImporter importer) {
		for (String mimeType: importer.handledContentTypes())
			importerMap.put(mimeType, importer);
	}
	
	@Override
	public void addNeutralWord(String word) {
		String lcWord = word.toLowerCase();
		if (!neutralWords.contains(lcWord))
			neutralWords.add(lcWord);
	}

	@Override
	public void removeNeutralWord(String word) {
		neutralWords.remove(word.toLowerCase());
	}

	@Override
	public String[] getNeutralWords() {
		return neutralWords.toArray(new String[0]);
	}
	
	public class WikiContent implements IWikiContent {
		public WikiContent() {}
		
		@Override
		public String getText() { return text; }

		@Override
		public void setText(String text) { this.text = text; changed = true; }

		@Override
		public String getMetadata(MetadataType key) {
			return metadata.get(key);
		}

		@Override
		public void setMetadata(MetadataType key, String value) {
			metadata.put(key, value);
			changed = true;
		}

		@Override
		public int getRevision() {
			return revision;
		}
		
		@Override
		public void setRevision(int revision) {
			this.revision = revision;
			changed = false;	// so this isn't marked as "changed" when loading from storage
		}

		@Override
		public void rollBack(int revision) throws DataStoreException {
			assert (revision > 0 && revision <= this.revision);
			
			WikiContent oldContent = (WikiContent) dataStore.get(oldName==null ? name : oldName, revision);
			
			// now we have to copy the fields from the retrieved version into ours...
			this.copy(oldContent);
			
			changed = true;
			rolledBack = true;
		}
		
		@Override
		public void update() throws DataStoreException {
			WikiContent dbContent = (WikiContent) dataStore.get(oldName==null ? name : oldName, 0);
			this.copy(dbContent);
			changed = false;
			rolledBack = false;
		}
		
		@Override
		public Boolean isUpToDate() throws DataStoreException {
			WikiContent dbContent = (WikiContent) dataStore.get(oldName==null ? name : oldName, 0);
			return this.revision==dbContent.revision;
		}
		
		@Override
		public String getName() { return name; }
		
		@Override
		public void setName(String newName) {
			if (oldName == null) oldName = name;
			name = newName;
			changed = true;
		}
		
		@Override
		public void save() throws DataStoreException {
			if (!changed)
				return;
			
			if (!rolledBack) ++revision;	// if a roll-back was performed leave the revision number as it was loaded from history!
			
			if (oldName != null) {
				// rename has been performed - commit to the datastore
				dataStore.rename(oldName, name);
				oldName = null;
			}
			
			dataStore.put(this, revision==0
								? true	/* this hasn't been saved yet, INSERT into data-store */
								: false	/* UPDATE data-store */);
			
			changed = false;
			rolledBack = false;
		}
		
		@Override
		public void apply() {
			changed = false;
		}
		
		@Override
		public void delete() throws DataStoreException {
			dataStore.delete(name);
			// reflect this by nullifying all existing internal state...
			oldName = null;
			text = null;
			metadata.clear();
			revision = 0;
			changed = false;
			rolledBack = false;
		}
		
		/**
		 * Helper method. Modify all fields of <tt>this</tt> to mirror those of the
		 * source.
		 * @param src	The source to copy from.
		 */
		private void copy(WikiContent src) {
			Field fields[] = this.getClass().getDeclaredFields();
			
			for (Field field : fields) {
				int mod = field.getModifiers();
				if (Modifier.isFinal(mod) || Modifier.isStatic(mod))
					continue;
				try {
					field.set(this, field.get(src));
				} catch (IllegalArgumentException e) {
					assert false;	// never happens
				} catch (IllegalAccessException e) {
					assert false;	// never happens
				}
			}
		}
		
		protected String oldName = null;	/* previous name - non-null only when a rename operation was invoked but
											   not yet committed */
		protected String name;
		protected String text;
		protected Map<MetadataType, String> metadata =
			new EnumMap<MetadataType, String>(MetadataType.class);
		protected int revision = 0;
		protected Boolean changed = true;		/* when the object is created it hasn't been saved to storage yet,
												   so it is considered "changed" */
		protected Boolean rolledBack = false;
	}
	
	protected Map<String, IContentImporter> importerMap = 
		new Hashtable<String, IContentImporter>();
	protected IContentRanker ranker;
	protected final IDataStore dataStore;
	protected List<String> neutralWords =
		new LinkedList<String>();
}
